package quarto.controller;

import java.util.ArrayList;

import quarto.Message;
import quarto.model.Board;
import quarto.model.Piece;
import quarto.controller.client.ClientHandler;
import quarto.controller.server.Server;

/**
 * Representatie van een spelletje Quarto
 * @knows 
 *  - De Server, twee ClientHandlers en Board
 *  - Wie er aan de beurt is
 *  - Welke Piece gespeeld moet worden
 * @does
 *  - Verwerkt input van de ClientHandlers
 *  - Houdt de ClientHandlers en Server op de hoogte van het spel
 * @author Frank Borgonjen & Tim Smit
 */
public class Game implements Protocol
{
    /** aantal spelers */
    public static final int NRPLAYERS = 2;
    
    /** het server object */
    private Server server;
    
    /** de clienthandlers */
    private ClientHandler[] clientHandlers;
    
    /** de namen van de clients */
    private String[] clientNames;
    
    /** het bord */
    private Board board;
    
    /** kan er in het spelletje gechat worden tussen de clients */
    private boolean canChat;
    
    /** geeft aan of het spel voorbij is */
    private boolean gameover;
    
    /** het stuk dat gezet moet worden */
    private Piece currentPiece;
    
    /** Array index van de Client die momenteel aan de beurt is */
    private int currentClientIndex;
    
    /** Array index van de Client die als laatste een bericht heeft verstuurd */
    private int senderIndex;
    
    /** Array van toegestane commando's */
    private ArrayList<Command> commands;

    /**
     * Construeert een nieuw Game Object met een server en twee clientHandlers
     * Een clientHandler is de link tussen de server en de client.
     * @param server het server object waar status berichten naartoe gestuurd
     *        kunnen worden
     * @param clientOne de eerste clientHandler
     * @param clientTwo de tweede clientHandler
     */
    public Game(Server server, ClientHandler clientOne, ClientHandler clientTwo)
    {
        this.server = server;
        this.clientHandlers = new ClientHandler[NRPLAYERS];
        this.clientHandlers[0] = clientOne;
        this.clientHandlers[1] = clientTwo;
        this.clientNames = new String[NRPLAYERS];
        this.clientNames[0] = clientOne.getClientName();
        this.clientNames[1] = clientTwo.getClientName();
        this.board = new Board();
        this.gameover = false;

        // Als beide clients dezelfde naam hebben wordt er bij de tweede
        // naam een random getal achter gezet
        if (clientNames[0].equals(clientNames[1]))
        {
            this.clientNames[1] = this.clientNames[1] + (int) (Math.random() * 10);
            this.clientHandlers[1].setClientName(this.clientNames[1]);
        }

        // boolean opslaan die aangeeft of beide Clients de chat functionaliteit
        // hebben geimplementeerd
        this.canChat = this.clientHandlers[0].canChat() &&
                       this.clientHandlers[1].canChat();

        // vul de lijst van commando's met alle commando's die Game 
        // zou moeten ondersteunen
        this.makeCommands();

        // Het spel beginnen
        this.startGame();
    }

    /**
     * Brengt de Players op de hoogte van wat hun namen zijn en start het spel 
     * door aan de eerste speler door te geven dat hij een stuk mag plaatsen.
     */
    private void startGame()
    {
        // bericht aan client1
        String msg1 = START + DELIM + clientNames[0] + DELIM + clientNames[1] + DELIM + canChat;
        this.sendSingleClient(clientHandlers[0], msg1);

        // bericht aan client2
        String msg2 = START + DELIM + clientNames[1] + DELIM + clientNames[0] + DELIM + canChat;
        this.sendSingleClient(clientHandlers[1], msg2);

        // Speler 1 op de hoogte brengen dat hij mag beginnen met het
        // eerste stuk (het lichte, grote, ronde, dichte stuk)
        this.currentClientIndex = 0;
        this.currentPiece = new Piece(0, 1, 1, 0);

        String msg3 = MAKEMOVE + DELIM + this.currentPiece.getNetworkNotation();
        this.sendSingleClient(clientHandlers[currentClientIndex], msg3);
    }

    /**
     * Vult de lijst van commando's met commando's die door Game uitgevoerd
     * kunnen worden.
     */
    public void makeCommands()
    {
        this.commands = new ArrayList<Command>();

        String regex;
        String d = "\\" + DELIM;

        // Het commando die de positie van het huidige stuk en het nieuwe 
        // stuk doorgeeft.
        regex = "^" + MOVE + d + // commando
            "(\\d+)" + // integer waarde van locatie
            "(" + d + "(" + TRUE + d + "|" + FALSE + // --------------
            d + "){3}(" + TRUE + "|" + FALSE + "))?$"; // het stuk
        commands.add(new MoveCommand(regex));

        // Het commando die binnenkomt als een van de clients een bericht 
        // via de chat stuurt
        regex = "^" + MSG + d + // commando
            "([^" + DELIM + "]+)$"; // het bericht
        commands.add(new MsgCommand(regex));

        // Het commando die aangeeft dat een client vertrokken is
        regex = "^" + QUIT + "$"; // commando
        commands.add(new QuitCommand(regex));
    }

    /**
     * Wordt aangeroepen vanuit een van de ClientHandlers
     * Krijgt een string representatie van een commando en kijkt of er in de
     * lijst van beschikbare commando's eentje zit die uitvoerbaar is.
     * Zodra er geen commando gevonden is in de lijst zal er een foutmelding
     * verstuurd worden naar de output.
     * @param sender de ClientHandler waar het commando vandaan komt
     * @param commandString de string representatie van een commando
     */
    public synchronized void executeCommand(ClientHandler sender, String commandString)
    {
        // index van de verzendende client opslaan
        senderIndex = getIndex(sender);
        
        this.notifyServer(String.format(Message.GAME_CLIENT_TO_SERVER, sender.getClientName(), commandString));
        
        boolean found = false;
        int i = 0;
        
        while (!found && i < this.commands.size())
        {
            if (this.commands.get(i).executable(commandString))
            {
                found = true;
                new Thread(this.commands.get(i)).start();
            }
            
            i++;
        }
        
        // er is geen geldig commando gestuurd UI en Client op de hoogte brengen
        if (!found)
        {
            String msg = String.format(Message.ERROR_INVALID_COMMAND, commandString);
            this.notifyServer(msg);
            
            // als de afzender kan chatten, sturen we een foutmelding
            if (this.clientHandlers[this.senderIndex].canChat())
            {
                this.sendSingleClient(clientHandlers[this.senderIndex], MSG + DELIM + msg);
            }
        }
    }

    /**
     * Verstuurt een bericht naar beide clients via de clientHandlers
     * @require msg != null
     */
    public void sendBothClients(String msg)
    {
        for (ClientHandler ch : this.clientHandlers)
        {
            this.sendSingleClient(ch, msg);
        }
    }

    /**
     * Verstuurt een bericht naar de client via de meegegeven clientHandler
     * @require msg != null
     * @param ch  clientHandler via welk het bericht verstuurd wordt
     * @param msg het bericht dat verstuurd wordt
     */
    public void sendSingleClient(ClientHandler ch, String msg)
    {
        this.notifyServer(String.format(Message.GAME_SERVER_TO_CLIENT, ch.getClientName(), msg));
        ch.sendMessage(msg);
    }

    /**
     * Brengt de server op de hoogte met een bericht
     * @param msg het bericht dat naar de server verstuurd wordt
     */
    public void notifyServer(String msg)
    {
        if (this.server != null)
        {
            this.server.addMessage(msg);
        }
        else
        {
            System.out.println(msg);
        }
    }

    /**
     * Geeft de index van de meegegeven clientHandler terug door de lijst
     * van clientHandlers af te lopen.
     * @param  clientHandler de clientHandler waarvan we de index zoeken
     * @return index van de clientHandler in de lijst
     * @ensure 0 <= result <= NRPLAYERS
     */
    private int getIndex(ClientHandler clientHandler)
    {
        int res = 0;
        int i = 0;
        while (i < this.clientHandlers.length)
        {
            if (this.clientHandlers[i] == clientHandler)
            {
                res = i;
            }
            
            i++;
        }
        
        return res;
    }
    
    /**
     * Wordt aangeroepen zodra een server afgesloten wordt. Het spel moet
     * nog niet afgesloten zijn.
     * Stuurt de clients een bericht hiervan en stuurt een GAMEOVER
     * zodat het een REMISE wordt.
     */
    public void closeGame()
    {
        if (!this.gameover)
        {
            // bericht sturen dat de server afgesloten is
            this.sendBothClients(MSG + DELIM + Message.GAME_ERROR_SERVER_QUIT);
            
            // remise
            this.sendBothClients(GAMEOVER);
            
            this.gameover = true;
        }
    }

    /**
     * Dit zijn alle Command klassen die gebruikt worden bij het 
     * uitvoeren van commando's aan de server (Game) kant
     */
    
    /** 
     * Wordt aangeroepen door de huidige client die een locatie en een nieuw
     * stuk heeft geselecteerd
     */
    private class MoveCommand extends Command
    {
        public MoveCommand(String regex)
        {
            super(regex);
        }

        /**
         * zodra de move toegestaan is doen we de zet en brengen we de clients
         * op de hoogte van de zet. Daarna kijken we of het spel afgelopen is.
         * Zo ja, dan kijken we of er een winnaar is en dat sturen we door naar
         * de clients. Zo niet, dan mag de volgende speler een zet doen met het
         * nieuwe huidige stuk.
         */
        public void run()
        {
            String msg;

            // Even kijken of de client wel aan de beurt is.
            if (senderIndex != currentClientIndex)
            {
                msg = MSG + DELIM + Message.GAME_ERROR_NOT_ON_TURN;
                sendSingleClient(clientHandlers[senderIndex], msg);
            }
            else
            {
                ArrayList<String> params = this.getParameters();
                boolean invalidMove = false;
                int position = Integer.parseInt(params.get(0));
                Piece newPiece = null;

                // kijken of de positie op het bord wel vrij is
                if (!board.isEmpty(position))
                {
                    invalidMove = true;

                    // zeggen dat de client een verkeerde locatie doorgegeven heeft
                    if (clientHandlers[currentClientIndex].canChat())
                    {
                        msg = MSG + DELIM + Message.GAME_ERROR_INVALID_LOCATION;
                        sendSingleClient(clientHandlers[currentClientIndex], msg);
                    }
                }
                // kijken of er een nieuw stuk meegestuurd is
                else if (params.size() > 1)
                {
                    newPiece = new Piece(params.get(1), params.get(2),
                        params.get(3), params.get(4));

                    if (!board.isAvailablePiece(newPiece) || 
                        currentPiece.compare(newPiece))
                    {
                        invalidMove = true;
                        if (clientHandlers[currentClientIndex].canChat())
                        {
                            msg = MSG + DELIM + Message.GAME_ERROR_UNAVAILABLE_PIECE;
                            sendSingleClient(clientHandlers[currentClientIndex], msg);
                        }
                    }
                }

                // move is niet toegestaan omdat positie en/of stuk niet geldig waren
                if (invalidMove)
                {
                    // nog eens een zet laten doen
                    msg = MAKEMOVE + DELIM + currentPiece.getNetworkNotation();
                    sendSingleClient(clientHandlers[currentClientIndex], msg);
                }
                // de move is wel toegestaan
                else
                {
                    // huidige piece op de meegegeven locatie zetten
                    board.setPiece(position, currentPiece);

                    // doorgeven dat het stuk geplaatst is
                    msg = MOVED + DELIM + position + DELIM + currentPiece.getNetworkNotation();
                    sendBothClients(msg);

                    // kijken of het spel voorbij is. En zo ja, of er een winnaar is en
                    // dat doorsturen naar de clients
                    if (board.gameOver())
                    {
                        gameover = true;
                        if (board.hasWinner())
                        {
                            msg = GAMEOVER + DELIM + clientNames[currentClientIndex];
                        }
                        else
                        {
                            msg = GAMEOVER;
                        }
                        
                        sendBothClients(msg);
                    }
                    // spel is nog niet voorbij, volgende speler is aan de beurt met het
                    // nieuwe stuk.
                    else
                    {
                        // currentPiece aanpassen in het meegegeven stuk
                        currentPiece = newPiece;

                        // index van de huidige client aanpassen en deze nieuwe
                        // huidige client een bericht sturen dat hij het stuk mag plaatsen
                        currentClientIndex = (currentClientIndex + 1) % NRPLAYERS;

                        msg = MAKEMOVE + DELIM + currentPiece.getNetworkNotation();
                        sendSingleClient(clientHandlers[currentClientIndex], msg);

                        // bord laten zien
                        System.out.println(board.toString());
                    }
                }
            }
        }
    }

    /**
     * Het berichten commando
     */
    private class MsgCommand extends Command
    {
        public MsgCommand(String regex)
        {
            super(regex);
        }

        /**
         * Voert alle handelingen uit die horen bij het bericht
         */
        public void run()
        {
            ArrayList<String> params = this.getParameters();
            // naam van de sender ervoor zetten en versturen naar beide clients
            sendBothClients(MSG + DELIM + String.format(Message.GAME_CHAT, clientNames[senderIndex],params.get(0)));
        }
    }

    /**
     * Representatie van het sluit commando
     */
    private class QuitCommand extends Command
    {
        public QuitCommand(String regex)
        {
            super(regex);
        }

        /**
         * Voert het daadwerkelijke afsluiten uit
         */
        public void run()
        {
            // server vertellen dat de client weg is
            notifyServer(String.format(Message.ELVIS_HAS_LEFT_THE_BUILDING, clientNames[senderIndex]));

            if (!gameover)
            {
                int otherIndex = (senderIndex + 1) % NRPLAYERS;

                // even tegen de tegenstander zeggen dat de client vertrokken is
                String msg = MSG + DELIM + String.format(Message.ELVIS_HAS_LEFT_THE_BUILDING, clientNames[senderIndex]);
                sendSingleClient(clientHandlers[otherIndex], msg);

                // doorgeven dat de andere speler gewonnen heeft
                msg = GAMEOVER + DELIM + clientNames[otherIndex];
                sendSingleClient(clientHandlers[otherIndex], msg);

                gameover = true;
            }
        }
    }
}
